package org.springframework.data.jpa.repository.support;

import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import spring.data.jpa.repository.CommonJpaRepository;
import spring.data.jpa.repository.support.SpecificationImpl;

@NoRepositoryBean
public class SimpleGenericJpaRepository<T, D> extends SimpleJpaRepository<T, D> implements CommonJpaRepository<T, D> {

    private final EntityManager em;

    public SimpleGenericJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.em = entityManager;
    }

    public SimpleGenericJpaRepository(Class<T> domainClass, EntityManager em) {
        this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
    }

    public Class<T> getJavaType() {
        return getDomainClass();
    }

    private <X> TypedQuery<X> getQuery(Class<X> clazz, Specification<X> spec, Pageable pageable) {
        return getQuery(clazz, spec, pageable.getSort());
    }

    private <X> TypedQuery<X> getQuery(Class<X> clazz, Specification<X> spec, Sort sort) {

        CriteriaBuilder builder = this.em.getCriteriaBuilder();
        CriteriaQuery<X> query = builder.createQuery(clazz);

        Root<X> root = applySpecificationToCriteria(clazz, spec, query);
        query.select(root);

        if (sort.isSorted()) {
            query.orderBy(toOrders(sort, root, builder));
        }

        TypedQuery<X> typedQuery = this.em.createQuery(query);
        CrudMethodMetadata metadata = getRepositoryMethodMetadata();
        if (metadata == null) {
            return typedQuery;
        }
        LockModeType type = metadata.getLockModeType();
        TypedQuery<X> toReturn = type == null ? typedQuery : typedQuery.setLockMode(type);

        for (Entry<String, Object> hint : super.getQueryHints().withFetchGraphs(em)) {
            toReturn.setHint(hint.getKey(), hint.getValue());
        }

        return toReturn;
    }

    private <X> TypedQuery<Long> getCountQuery(Class<X> clazz, Specification<X> spec) {

        CriteriaBuilder builder = this.em.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);

        Root<X> root = applySpecificationToCriteria(clazz, spec, query);
        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            query.select(builder.count(root));
        }

        return this.em.createQuery(query);
    }

    private Query getCountQuery(String jpql, Object... values) {
        String countJpql = jpql.replaceFirst("^[select|SELECT]{0,1}.+[from|FROM]\\s", COUNT_SQL);
        return getQuery(countJpql, values);
    }

    private Query getCountQuery(String jpql, Map<String, Object> values) {
        String countJpql = jpql.replaceFirst("^[select|SELECT]{0,1}.+[from|FROM]\\s", COUNT_SQL);
        return getQuery(countJpql, values);
    }

    /**
     * 根据jpql创建query,jpql中的参数请从1开始顺序绑定
     * 
     * @param jpql
     * @param values
     * @return
     */
    private Query getQuery(String jpql, Object... values) {
        Assert.hasText(jpql, "jpql不能为空.");
        Query query = this.em.createQuery(jpql);
        for (int i = 0; i < values.length; i++) {
            query.setParameter(i + 1, values[i]);
        }
        return query;
    }

    /**
     * 根据jpql创建query,jpql中的参数请从1开始顺序绑定
     * 
     * @param jpql
     * @param values
     * @return
     */
    private Query getQuery(String jpql, Map<String, Object> values) {
        Assert.hasText(jpql, "jpql不能为空.");
        Query query = this.em.createQuery(jpql);
        for (Entry<String, Object> entry : values.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return query;
    }

    private <X> Root<X> applySpecificationToCriteria(Class<X> clazz, Specification<X> spec, CriteriaQuery<?> query) {
        Assert.notNull(query, "CriteriaQuery can not be null.");
        Root<X> root = query.from(clazz);
        CriteriaBuilder builder = this.em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);
        if (predicate != null) {
            query.where(predicate);
        }
        return root;
    }

    @Override
    public List<T> findOrderTop(Specification<T> spec, Sort sort, int size) {
        return getQuery(spec, sort).setFirstResult(0).setMaxResults(size).getResultList();
    }

    @Override
    public List<T> findTop(Specification<T> spec, int size) {
        return getQuery(spec, Sort.unsorted()).setFirstResult(0).setMaxResults(size).getResultList();
    }

    @Override
    public List<T> findOrderTop(Sort sort, int size) {
        return getQuery(null, sort).setFirstResult(0).setMaxResults(size).getResultList();
    }

    @Override
    public List<T> findTop(int size) {
        return getQuery(null, Sort.unsorted()).setFirstResult(0).setMaxResults(size).getResultList();
    }

    @Override
    @Transactional(readOnly = true)
    public <X, O> X findOne(Class<X> clazz, O id) {
        Assert.notNull(clazz, "The given class must not be null!");
        return this.em.find(clazz, id);
    }

    @Transactional(readOnly = true)
    public <X> X findOne(Class<X> clazz, Specification<X> spec) {
        Assert.notNull(clazz, "The given class must not be null!");
        return getQuery(clazz, spec, Sort.unsorted()).getSingleResult();
    }

    @Transactional
    public <X> X save(Class<X> clazz, X entity) {
        JpaEntityInformation<X, ?> information = JpaEntityInformationSupport.getEntityInformation(clazz, this.em);
        if (information.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

    @Transactional
    public <X, O> void remove(Class<X> clazz, O id) {
        X x = findOne(clazz, id);
        remove(x);
    }

    @Transactional
    public <X> void remove(X entity) {
        Assert.notNull(entity, "The entity must not be null!");
        this.em.remove(this.em.contains(entity) ? entity : this.em.merge(entity));
    }

    @Transactional(readOnly = true)
    public <X> List<X> findAll(Class<X> clazz) {
        Specification<X> spec = new SpecificationImpl<>(clazz);
        return getQuery(clazz, spec, Sort.unsorted()).getResultList();
    }

    @Transactional(readOnly = true)
    public <X> List<X> findAll(Class<X> clazz, Sort sort) {
        Specification<X> spec = new SpecificationImpl<>(clazz);
        return getQuery(clazz, spec, sort).getResultList();
    }

    @Transactional(readOnly = true)
    public <X> List<X> findAll(Class<X> clazz, Specification<X> spec) {
        return getQuery(clazz, spec, Sort.unsorted()).getResultList();
    }

    @Transactional(readOnly = true)
    public <X> Page<X> findPage(Class<X> clazz, Specification<X> spec, Pageable pageable) {
        TypedQuery<X> query = getQuery(clazz, spec, pageable);
        return readPage(clazz, query, pageable, spec);
    }

    private static Long executeCountQuery(TypedQuery<Long> query) {

        Assert.notNull(query, "TypedQuery can not be null.");

        List<Long> totals = query.getResultList();
        Long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }

        return total;
    }

    private <X> Page<X> readPage(Class<X> clazz, TypedQuery<X> query, Pageable pageable, Specification<X> spec) {

        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());

        Long total = executeCountQuery(getCountQuery(clazz, spec));
        List<X> content = total > pageable.getOffset() ? query.getResultList() : Collections.<X> emptyList();

        return new PageImpl<>(content, pageable, total);
    }

    @Transactional(readOnly = true)
    public <X> List<X> findAll(Class<X> clazz, Specification<X> spec, Sort sort) {
        return getQuery(clazz, spec, sort).getResultList();
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public <X> List<X> findAll(String jpql, Object... values) {
        return (List<X>) getQuery(jpql, values).getResultList();
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public <X> List<X> findAll(String jpql, Map<String, Object> values) {
        return (List<X>) getQuery(jpql, values).getResultList();
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public <X> Page<X> findPage(String jpql, Pageable pageable, Object... values) {
        Query query = getQuery(jpql, values);
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());

        Long total = ((Number) getCountQuery(jpql, values).getSingleResult()).longValue();
        List<X> content = total > pageable.getOffset() ? query.getResultList() : Collections.<X> emptyList();
        return new PageImpl<>(content, pageable, total);
    }

    @Transactional(readOnly = true)
    @SuppressWarnings("unchecked")
    public <X> Page<X> findPage(String jpql, Pageable pageable, Map<String, Object> values) {
        Query query = getQuery(jpql, values);
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());

        Long total = ((Number) getCountQuery(jpql, values).getSingleResult()).longValue();
        List<X> content = total > pageable.getOffset() ? query.getResultList() : Collections.<X> emptyList();
        return new PageImpl<>(content, pageable, total);
    }

    @Override
    @Transactional
    public int execute(String jpql, Object... values) {
        return getQuery(jpql, values).executeUpdate();
    }

    @Override
    @Transactional
    public int execute(String jpql, Map<String, Object> values) {
        return getQuery(jpql, values).executeUpdate();
    }

    @Override
    public EntityManager getEntityManager() {
        return this.em;
    }
}
